TP-Link Archer路由器LAN RCE

    消息类型: 公司新闻         发布日期:2020-04-22

                         

1、概述

TP-Link Archer系列路由器是普联(TP-Link)公司的无线路由产品,TP-Link Archer A7/C7 MIPS架构、固件版本为190726的路由器产品,其tdpServer服务存在命令注入漏洞,LAN侧的未授权攻击者可利用漏洞以root权限进行任意代码执行。该漏洞由Flashback团队(Pedro Ribeiro + Radek Domanski)在Pwn2Own Tokyo 2019活动中发现。

本文参考了相关资料,对ZDI博客进行了理解、基本翻译和相关验证工作,如有不当之处请见谅。以下内容已形成完整实验课程,并集成入“凯发k8App工控网络攻防实训平台”中使用。

2、环境配置

TP-Link官网可下载对应版本固件:https://static.tp-link.com/2019/201908/20190816/Archer%20C7(US)_V5_190726.zip,尝试使用firmadyne工具运行固件:


但发现网络分配步骤失败:

 

进而运行时报错:

 

使用Qemu MIPS虚拟机加载固件运行,找到目标文件tdpServer并尝试执行:

 

 

发现执行出错。分析固件仿真启动和执行tdpServer失败的原因,估计是因为路由器在启动中,部分调用资源在其他硬件flash中,因而缺少资源导致失败,最好的方式不过为购买一台TP-Link Archer A7进行试验了。

3、漏洞分析

tdpServer守护进程在本地0.0.0.0接口的UPD 20002端口监听,为TP-Link移动应用程序对路由器建立控制提供基础,守护进程与应用程序之间通过使用带有加密payload的UDP数据包通信,逆向分析得到数据包格式如下:

 

数据包的格式决定了守护进程提供的服务,例如type为0,守护进行将提供tdpd服务,使用特定的TETHER_KEY哈希值的数据包进行回复,但此处与漏洞无关;type为0xf0, 守护进行将提供onemesh服务, onemesh在许多TP-Link新固件中均引入了该项新技术,具体可参考( https://www.tp-link.com/us/onemesh/compatibility/)。

设备启动后,函数调用顺序为:tdpd_pkt_handler_loop()(地址:0x40d164)->监听端口20002接收到数据传递->tpdp_pkt_parser()(地址:0x40cfe0),tpdp_pkt_parser()函数伪码如下:


undefined4 FUN_0040cfe0(int iParm1,int iParm2,int iParm3,int *piParm4,int param_5)


{

  byte bVar1;

  uint uVar2;

  undefined2 uVar5;

  __pid_t _Var3;

  int iVar4;

  uint uVar6;

  char *pcVar7;

  char *pcVar8;

  int local_c8 [42];

  ushort local_20 [2];

  int local_1c;

  int local_18;


  if (iParm1 != 0) {

    iVar4 = FUN_0040d600();

    if (iParm2 < iVar4) {

      FUN_00403734("tdpdServer.c:709","recvbuf length = %d, less than hdr\'s 16",iParm2);

      return 0xffffffff;

    }

    iVar4 = FUN_0040d620(iParm1);

    if (iVar4 < 1) {

      pcVar7 = "tdpdServer.c:716";

      pcVar8 = "tdp pkt is too big";

    }

    else {

      FUN_00403734("tdpdServer.c:719","tdp pkt length is %d",iVar4);

      iVar4 = FUN_0040c9d0(iParm1,iVar4);

      if (iVar4 < 0) {

        return 0xffffffff;

      }

      if (*(char *)(iParm1 + 1) == 0) {

        local_20[0] = 0;

        if (iParm1 != 0) {

          if (iParm3 == 0) {

            return 0xffffffff;

          }

          if (piParm4 != (int *)0x0) {

            bVar1 = *(byte *)(iParm1 + 6);

            uVar2 = 1;

            if ((bVar1 & 0x10) == 0) {

              uVar2 = ((uint)bVar1 << 0x1a) >> 0x1f;

            }

            uVar6 = (uint)bVar1 & 1;

            if (uVar2 == 0) {

              uVar6 = 0;

            }

            if (uVar6 == 0) {

              pcVar7 = "tdpdServer.c:837";

              pcVar8 = "TDP flag error";

            }

            else {

              if (*(short *)(iParm1 + 2) == 2) {

                local_1c = 0;

                local_18 = 0;

                iVar4 = pipe((int *)&stack0xffffffe4);

                if (iVar4 < 0) {

                  if (0 < local_1c) {

                    close(local_1c);

                  }

                  if (local_18 < 1) {

                    return 0xffffffff;

                  }

                  close(local_18);

                  return 0xffffffff;

                }

                _Var3 = fork();

                if (_Var3 == -1) {

                  return 0xffffffff;

                }

                if (_Var3 == 0) {

                  close(local_1c);

                  local_20[0] = FUN_0040b804((void *)(iParm3 + 0x10),param_5);

                  write(local_18,local_20,2);

                  write(local_18,(void *)(iParm3 + 0x10),(uint)local_20[0]);

                  close(local_18);

                    /* WARNING: Subroutine does not return */

                  exit(0);

                }

                close(local_18);

                wait((void *)0x0);

                read(local_1c,local_20,2);

                read(local_1c,(void *)(iParm3 + 0x10),(uint)local_20[0]);

                close(local_1c);

                uVar5 = *(undefined2 *)(iParm1 + 2);

              }

              else {

                local_20[0] = FUN_0040b7d8(iParm3 + 0x10);

                uVar5 = *(undefined2 *)(iParm1 + 2);

              }

              *(undefined2 *)(iParm3 + 2) = uVar5;

              *(byte *)(iParm3 + 6) = *(byte *)(iParm1 + 6) & 0xfe | 2;

              *(undefined *)(iParm3 + 7) = 1;

              *(ushort *)(iParm3 + 4) = local_20[0];

              *(undefined4 *)(iParm3 + 8) = *(undefined4 *)(iParm1 + 8);

              iVar4 = FUN_0040cb20(iParm3);

              if (-1 < iVar4) {

                *piParm4 = iVar4;

                return 0;

              }

              pcVar7 = "tdpdServer.c:844";

              pcVar8 = "TDP encode pkt error";

            }

            FUN_00403734(pcVar7,pcVar8);

          }

        }

        return 0xffffffff;

      }

      if ((*(char *)(iParm1 + 1) == -0x10) && (DAT_0042f0f0 == 1)) {

        if ((iParm1 != 0) &&

           (((iParm3 != 0 && (piParm4 != (int *)0x0)) &&

            (iVar4 = FUN_0040e074(local_c8,iParm2), iVar4 == 0)))) {

          FUN_00403734("tdpdServer.c:883","recv ip is %x, my ip is %x",param_5,local_c8[0]);

          if (param_5 == local_c8[0]) {

            FUN_00403734("tdpdServer.c:886","Ignore onemesh tdp packet to myself...");

          }

          else {

            FUN_00403734("tdpdServer.c:890","opcode %x, flags %x",(uint)*(ushort *)(iParm1 + 2),

                         (uint)*(byte *)(iParm1 + 6));

            switch((uint)*(ushort *)(iParm1 + 2) - 1 & 0xffff) {

            case 0:

              if ((*(byte *)(iParm1 + 6) & 1) == 0) {

                pcVar7 = "tdpdServer.c:904";

                pcVar8 = "Invalid flags";

              }

              else {

                iVar4 = FUN_00412ed4(iParm1,iParm2,iParm3,piParm4,param_5);

                if (-1 < iVar4) {

                  return 0;

                }

                pcVar7 = "tdpdServer.c:898";

                pcVar8 = "error processing probe request...";

              }

              break;

            case 1:

              if ((*(byte *)(iParm1 + 6) & 1) != 0) {

                return 0;

              }

              pcVar7 = "tdpdServer.c:915";

              pcVar8 = "Invalid flags";

              break;

            default:

              pcVar7 = "tdpdServer.c:966";

              pcVar8 = "Invalid operation!";

              break;

            case 3:

              if ((*(byte *)(iParm1 + 6) & 1) == 0) {

                pcVar7 = "tdpdServer.c:931";

                pcVar8 = "Invalid flags";

              }

              else {

                iVar4 = FUN_00414650(iParm1,iParm2,iParm3,piParm4,param_5);

                if (-1 < iVar4) {

                  return 0;

                }

                pcVar7 = "tdpdServer.c:925";

                pcVar8 = "error processing attach_master request...";

              }

              break;

            case 5:

              if ((*(byte *)(iParm1 + 6) & 1) != 0) {

                return 0;

              }

              pcVar7 = "tdpdServer.c:942";

              pcVar8 = "Invalid flags";

              break;

            case 6:

              if ((*(byte *)(iParm1 + 6) & 1) == 0) {

                pcVar7 = "tdpdServer.c:958";

                pcVar8 = "Invalid flags";

              }

              else {

                iVar4 = FUN_00414d14(iParm1,iParm2,iParm3,piParm4,param_5);

                if (-1 < iVar4) {

                  return 0;

                }

                pcVar7 = "tdpdServer.c:952";

                pcVar8 = "error processing slave_key_offer request...";

              }

            }

            FUN_00403734(pcVar7,pcVar8);

          }

        }

        return 0xffffffff;

      }

      pcVar7 = "tdpdServer.c:742";

      pcVar8 = "invalid tdp packet type";

    }

    FUN_00403734(pcVar7,pcVar8);

  }

  return 0xffffffff;

}


第一部分:检查数据包、校验和的验证;第二部分:type判断,即调用服务选择;第三部分:标志字段onemesh_flag为1,进入onemesh_main()(地址:0x40cd78),onemesh_main()根据操作码字段调用相应函数,举例:操作码为6——调用onemesh_slave_key_offer()(地址:0x414d14)

 

tpdp_pkt_parser()#1

首先检查UDP套接字标头大小是否至少为0x10;调用tdpd_get_pkt_len()(地址:0x40d620),该函数返回在包头中声明的包长度,如果数据包长度超过0x410,则此函数返回-1;最后再通过tdpd_pkt_sanity_checks()(地址:0x40c9d0),检查数据包版本(版本字段,数据包中的第一个字节)是否等于1,接着,使用自定义校验和函数tpdp_pkt_calc_checksum()(地址:0x4037f0)计算数据包的校验和。由于tpdp_pkt_calc_checksum()内容较多,借助lao_bomb漏洞利用代码的calc_checksum()分析:


lao_bomb漏洞利用代码的calc_checksum()

首先,在数据包的校验和字段中设置魔术变量0x5a6b7c8d,然后使用带有1024个字节的表reference_tbl来计算整个数据包(包括报头)的校验和;校验和通过验证并且所有结果正确之后,tdpd_pkt_sanity_checks()返回0。


tpdp_pkt_parser()#2

漏洞函数onemesh_slave_key_offer():第一部分:将payload传递给tpapp_aes_decrypt()(地址:0x40b190),功能为使用AES算法和静态密钥“TPONEMESH_Kf!xn?gj6pMAt-wBNV_TDP”解密payload;第二部分:对onemesh对象做一些设置后,解析payload(一个json对象)获取json键及其值;第三部分:按顺序处理获取的键与值(若键不存在,直接退出函数),json对象中的值传递给堆栈变量slaveMac、slaveIp等,调用create_csjon_obj()(地址:0x405fe8)函数处理;第四部分:create_csjon_obj()处理:堆栈变量slaveMac被传递给systemCmd变量,然后由system(systemCmd)执行。(函数太长,只看部分代码)

假设jason对象如下所示:

 

want_to_join必须为false,type设置为0xf0,opcode设置为6,将flags设置为1,并正确获取校验和字段。

数据包使用AES加密:固定密钥:TPONEMESH_Kf!xn?gj6pMAt-wBNV_TDP,使用CBC模式(IV固定值:1234567890abcdef1234567890abcdef),实际使用其中的128位密钥的AES-CBC,256位密钥和IV中有一半没有使用。


代码执行:

知道了到达漏洞代码的可行路径,发送数据包进行命令执行还需克服两个限制:1、strncpy()只从slave_mac_info键中的值拷贝0x11 字节(17个字节)递给slaveMac变量,且包括终止字符null2、slaveMac变量中的值有单双引号的使用,需要进行转义;

为了转义单双引号,需要添加:';',由此占用了3个字符,实际测试中payload剩下12个字符可用,如此一来几乎不能做什么有意义的命令执行。解决方法是多次触发漏洞,将命令写入文件最终当作shell脚本执行,例如:

Ø printf a>>z

将字符a追加到文件z当中,此payload用掉了11个字节。另外,shell会将数字解释为文件描述符,特殊字符"."或";"的无法写入;

Ø printf '1'>x

创建一个名为“x”的新文件,文件仅包含字符“1”,此payload用掉了12个字节,无法添加额外的“>” ;

Ø cat x*>>z*

对于数字或特殊字符,首先将字符写入新文件,然后使用cat将新文件的内容附加到正在构建的命令文件。由于文件z最终会被命名成z”}),因此在每个文件名后添加“*”,shell会使用特殊的'*'字符自动补全。

当前文件创建文件的位置在根目录下,通常嵌入是文件系统的根目录是不可写的,因此写文件需要到/tmp目录下操作,但TP-Link的根文件系统是以读写方式安装,由此节省很多字节cd tmp)。最后以root用户执行命令文件sh z。

4EXP

按照如上思路,可参照EXP代码对漏洞利用原理再次进行理解,有TP-Link Archer路由器设备的情况下可进行实际测试。总结整个漏洞利用实现的过程:在理解了数据通信原理和数据包格式的基础下,找到触发漏洞的可行途径,在漏洞执行存在极为苛刻的限制下,通过多次触发漏洞将命令写入文件中,最终当作shell脚本执行,从而达到任意代码执行的目的。




参考资料:

https://www.thezdi.com/blog/2020/4/6/exploiting-the-tp-link-archer-c7-at-pwn2own-tokyo

https://www.exploit-db.com/exploits/48331

服务热线
025-8660 3700

微信公众号